diff options
author | 2025-04-27 23:02:42 +0530 | |
---|---|---|
committer | 2025-04-27 23:02:42 +0530 | |
commit | 538d933baef56d7ee76f78617b553d63713efa24 (patch) | |
tree | 3fcbc4208849dfa0e5dc8fe5761e103a3591c283 /frontend/src/app/(main)/goals/[id]/page.tsx | |
parent | 3941d80ff120238b973451325b834ebd8377281e (diff) | |
download | finance-538d933baef56d7ee76f78617b553d63713efa24.tar.gz finance-538d933baef56d7ee76f78617b553d63713efa24.tar.bz2 finance-538d933baef56d7ee76f78617b553d63713efa24.zip |
finance: feat: added the goal page with some improvements of ui
Diffstat (limited to 'frontend/src/app/(main)/goals/[id]/page.tsx')
-rw-r--r-- | frontend/src/app/(main)/goals/[id]/page.tsx | 290 |
1 files changed, 290 insertions, 0 deletions
diff --git a/frontend/src/app/(main)/goals/[id]/page.tsx b/frontend/src/app/(main)/goals/[id]/page.tsx new file mode 100644 index 0000000..3428ca4 --- /dev/null +++ b/frontend/src/app/(main)/goals/[id]/page.tsx @@ -0,0 +1,290 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; +import { useRouter } from "next/navigation"; +import Link from "next/link"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Progress } from "@/components/ui/progress"; +import { Badge } from "@/components/ui/badge"; +import { Edit, ArrowLeft, Loader2, RefreshCw } from "lucide-react"; +import { useToast } from "@/components/ui/use-toast"; +import { formatCurrency } from "@/lib/utils"; +import { api } from "@/lib/api"; +import { GoalProgress } from "../components/goals-list"; + +export default function GoalDetailPage({ params }: { params: { id: string } }) { + const id = params.id; + const goalId = parseInt(id); + + const [goal, setGoal] = useState<GoalWithProgress | null>(null); + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + const router = useRouter(); + const { toast } = useToast(); + + const fetchGoalDetails = useCallback(async () => { + try { + console.log(`Fetching goal details for ID: ${goalId}`); + setLoading(true); + + // Add cache-busting parameter + const response = await api.get<GoalProgress>(`/goals/${goalId}/progress?cache=${new Date().getTime()}`); + console.log("Goal details received:", response.data); + + // Validate and normalize data + const data = response.data; + if (data && data.goal) { + const sanitizedData = { + ...data, + goal: { + ...data.goal, + targetAmount: Number(data.goal.targetAmount) || 0, + currentAmount: Number(data.goal.currentAmount) || 0, + createdAt: data.goal.createdAt || new Date().toISOString(), + }, + percentComplete: Number(data.percentComplete) || 0, + amountRemaining: Number(data.amountRemaining) || 0, + daysRemaining: Number(data.daysRemaining) || 0, + requiredPerDay: Number(data.requiredPerDay) || 0, + requiredPerMonth: Number(data.requiredPerMonth) || 0, + }; + console.log("Processed goal data:", sanitizedData); + setGoal(sanitizedData); + } else { + console.error("Invalid goal data format:", data); + throw new Error("Invalid goal data received"); + } + } catch (error) { + console.error("Error fetching goal details:", error); + toast({ + title: "Error", + description: "Failed to fetch goal details. Please try again.", + variant: "destructive", + }); + router.push("/goals"); + } finally { + setLoading(false); + } + }, [goalId, toast, router]); + + // Fetch goal details when component mounts + useEffect(() => { + if (!id) { + toast({ + title: "Error", + description: "Goal ID is missing. Please try again.", + variant: "destructive", + }); + router.push("/goals"); + return; + } + + fetchGoalDetails(); + }, [id, fetchGoalDetails, router, toast]); + + const recalculateProgress = async () => { + if (isNaN(goalId)) { + toast({ + title: "Error", + description: "Invalid goal ID", + variant: "destructive", + }); + return; + } + + try { + setRefreshing(true); + await api.post(`/goals/${goalId}/recalculate`); + toast({ + title: "Progress recalculated", + description: "Your goal progress has been recalculated based on transactions.", + }); + fetchGoalDetails(); + } catch (error) { + toast({ + title: "Error", + description: "Failed to recalculate goal progress. Please try again.", + variant: "destructive", + }); + console.error("Error recalculating goal progress:", error); + } finally { + setRefreshing(false); + } + }; + + if (loading) { + return ( + <div className="container mx-auto py-8 flex justify-center items-center"> + <Loader2 className="h-8 w-8 animate-spin" /> + </div> + ); + } + + if (!goal) { + return ( + <div className="container mx-auto py-8 text-center"> + <p className="mb-4">Goal not found or access denied.</p> + <Link href="/goals"> + <Button>Back to Goals</Button> + </Link> + </div> + ); + } + + const { goal: goalData, percentComplete, amountRemaining, daysRemaining, requiredPerDay, requiredPerMonth, onTrack } = goal; + const isCompleted = goalData.status === "Achieved"; + + return ( + <div className="container mx-auto py-8"> + <div className="mb-6"> + <Link href="/goals"> + <Button variant="ghost" size="sm"> + <ArrowLeft className="mr-2 h-4 w-4" /> + Back to Goals + </Button> + </Link> + </div> + + <div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-6"> + <div> + <h1 className="text-2xl font-bold tracking-tight">{goalData.name}</h1> + <p className="text-muted-foreground"> + {isCompleted + ? "Goal has been achieved 🎉" + : onTrack + ? "Progress is on track" + : "Progress is behind schedule"} + </p> + </div> + <div className="flex space-x-3 mt-4 md:mt-0"> + <Button + variant="outline" + size="sm" + onClick={recalculateProgress} + disabled={refreshing} + > + {refreshing ? ( + <Loader2 className="mr-2 h-4 w-4 animate-spin" /> + ) : ( + <RefreshCw className="mr-2 h-4 w-4" /> + )} + Recalculate + </Button> + <Link href={`/goals/edit/${goalData.id}`}> + <Button variant="outline" size="sm"> + <Edit className="mr-2 h-4 w-4" /> + Edit + </Button> + </Link> + </div> + </div> + + <div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> + <Card className="lg:col-span-2"> + <CardHeader> + <div className="flex justify-between items-center"> + <CardTitle>Goal Progress</CardTitle> + <Badge variant={isCompleted ? "default" : onTrack ? "outline" : "destructive"}> + {isCompleted ? "Achieved" : onTrack ? "On Track" : "Behind"} + </Badge> + </div> + </CardHeader> + <CardContent> + <div className="mb-6"> + <div className="flex justify-between mb-2"> + <span>Completion</span> + <span>{Math.round(percentComplete)}%</span> + </div> + <Progress value={percentComplete} className="h-3" /> + </div> + + <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> + <div className="space-y-4"> + <div> + <h3 className="text-sm font-medium text-muted-foreground mb-1">Target Amount</h3> + <p className="text-2xl font-semibold">{formatCurrency(goalData.targetAmount)}</p> + </div> + <div> + <h3 className="text-sm font-medium text-muted-foreground mb-1">Current Amount</h3> + <p className="text-2xl font-semibold">{formatCurrency(goalData.currentAmount)}</p> + </div> + <div> + <h3 className="text-sm font-medium text-muted-foreground mb-1">Remaining</h3> + <p className="text-2xl font-semibold">{formatCurrency(amountRemaining)}</p> + </div> + </div> + + <div className="space-y-4"> + {goalData.targetDate && ( + <div> + <h3 className="text-sm font-medium text-muted-foreground mb-1">Target Date</h3> + <p className="text-xl font-semibold">{new Date(goalData.targetDate).toLocaleDateString()}</p> + </div> + )} + {daysRemaining > 0 && ( + <> + <div> + <h3 className="text-sm font-medium text-muted-foreground mb-1">Days Remaining</h3> + <p className="text-xl font-semibold">{daysRemaining} days</p> + </div> + <div> + <h3 className="text-sm font-medium text-muted-foreground mb-1">Required Per Day</h3> + <p className="text-xl font-semibold">{formatCurrency(requiredPerDay)}</p> + </div> + <div> + <h3 className="text-sm font-medium text-muted-foreground mb-1">Required Per Month</h3> + <p className="text-xl font-semibold">{formatCurrency(requiredPerMonth)}</p> + </div> + </> + )} + </div> + </div> + </CardContent> + </Card> + + <Card> + <CardHeader> + <CardTitle>Goal Details</CardTitle> + </CardHeader> + <CardContent> + <div className="space-y-4"> + <div> + <h3 className="text-sm font-medium text-muted-foreground mb-1">Goal Name</h3> + <p className="font-medium">{goalData.name}</p> + </div> + <div> + <h3 className="text-sm font-medium text-muted-foreground mb-1">Purpose</h3> + <p>{goalData.name}</p> + </div> + <div> + <h3 className="text-sm font-medium text-muted-foreground mb-1">Status</h3> + <p>{goalData.status}</p> + </div> + <div> + <h3 className="text-sm font-medium text-muted-foreground mb-1">Created</h3> + <p>{new Date(goalData.createdAt).toLocaleDateString()}</p> + </div> + {isCompleted ? ( + <div className="pt-4"> + <div className="p-4 bg-green-50 dark:bg-green-950 text-green-700 dark:text-green-300 rounded-md"> + <p className="font-semibold">🎉 Goal achieved!</p> + <p className="text-sm mt-1"> + Congratulations on achieving your financial goal. + </p> + </div> + </div> + ) : ( + <div className="pt-4"> + <Link href={`/transactions?goalId=${goalData.id}`}> + <Button variant="secondary" className="w-full">View Related Transactions</Button> + </Link> + </div> + )} + </div> + </CardContent> + </Card> + </div> + </div> + ); +}
\ No newline at end of file |